{
 "cells": [
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:02:54.456231Z",
     "start_time": "2025-02-27T09:02:51.034518Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 2.0.2\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.1\n",
      "torch 2.6.0+cu126\n",
      "cuda:0\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据准备\n",
    "\n",
    "```shell\n",
    "$ tree -L 2 archive \n",
    "archive\n",
    "├── monkey_labels.txt\n",
    "├── training\n",
    "│   ├── n0\n",
    "│   ├── n1\n",
    "│   ├── n2\n",
    "│   ├── n3\n",
    "│   ├── n4\n",
    "│   ├── n5\n",
    "│   ├── n6\n",
    "│   ├── n7\n",
    "│   ├── n8\n",
    "│   └── n9\n",
    "└── validation\n",
    "    ├── n0\n",
    "    ├── n1\n",
    "    ├── n2\n",
    "    ├── n3\n",
    "    ├── n4\n",
    "    ├── n5\n",
    "    ├── n6\n",
    "    ├── n7\n",
    "    ├── n8\n",
    "    └── n9\n",
    "\n",
    "22 directories, 1 file\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:02:57.155322Z",
     "start_time": "2025-02-27T09:02:54.457195Z"
    }
   },
   "source": [
    "from torchvision import datasets\n",
    "from torchvision.transforms import ToTensor, Resize, Compose, ConvertImageDtype, Normalize\n",
    "\n",
    "\n",
    "from pathlib import Path\n",
    "\n",
    "DATA_DIR = Path(\"./archive/\")\n",
    "\n",
    "class MonkeyDataset(datasets.ImageFolder):\n",
    "    def __init__(self, mode, transform=None):\n",
    "        if mode == \"train\":\n",
    "            root = DATA_DIR / \"training\"\n",
    "        elif mode == \"val\":\n",
    "            root = DATA_DIR / \"validation\"\n",
    "        else:\n",
    "            raise ValueError(\"mode should be one of the following: train, val, but got {}\".format(mode))\n",
    "        super().__init__(root, transform)\n",
    "        self.imgs = self.samples\n",
    "        self.targets = [s[1] for s in self.samples]\n",
    "\n",
    "# resnet 要求的，见 https://pytorch.org/vision/stable/models/generated/torchvision.models.resnet50.html\n",
    "img_h, img_w = 224, 224\n",
    "transform = Compose([\n",
    "     Resize((img_h, img_w)),\n",
    "     ToTensor(),\n",
    "     Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n",
    "     ConvertImageDtype(torch.float),\n",
    "])\n",
    "\n",
    "\n",
    "train_ds = MonkeyDataset(\"train\", transform=transform)\n",
    "val_ds = MonkeyDataset(\"val\", transform=transform)\n",
    "\n",
    "print(\"load {} images from training dataset\".format(len(train_ds)))\n",
    "print(\"load {} images from validation dataset\".format(len(val_ds)))"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "load 1097 images from training dataset\n",
      "load 272 images from validation dataset\n"
     ]
    }
   ],
   "execution_count": 2
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:02:57.160203Z",
     "start_time": "2025-02-27T09:02:57.156295Z"
    }
   },
   "source": [
    "# 数据类别\n",
    "train_ds.classes, train_ds.class_to_idx"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9'],\n",
       " {'n0': 0,\n",
       "  'n1': 1,\n",
       "  'n2': 2,\n",
       "  'n3': 3,\n",
       "  'n4': 4,\n",
       "  'n5': 5,\n",
       "  'n6': 6,\n",
       "  'n7': 7,\n",
       "  'n8': 8,\n",
       "  'n9': 9})"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 3
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:02:57.187032Z",
     "start_time": "2025-02-27T09:02:57.161180Z"
    }
   },
   "source": [
    "# 图片路径 及 标签\n",
    "for fpath, label in train_ds.imgs:\n",
    "    print(fpath, label)\n",
    "    break\n",
    "\n",
    "for img, label in train_ds:\n",
    "    # c, h, w  label\n",
    "    print(img.shape, label)\n",
    "    break"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "archive\\training\\n0\\n0018.jpg 0\n",
      "torch.Size([3, 224, 224]) 0\n"
     ]
    }
   ],
   "execution_count": 4
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:02:57.193395Z",
     "start_time": "2025-02-27T09:02:57.188978Z"
    }
   },
   "source": [
    "# 遍历train_ds得到每张图片，计算每个通道的均值和方差\n",
    "def cal_mean_std(ds):\n",
    "    mean = 0.\n",
    "    std = 0.\n",
    "    for img, _ in ds:\n",
    "        mean += img.mean(dim=(1, 2))\n",
    "        std += img.std(dim=(1, 2))\n",
    "    mean /= len(ds)\n",
    "    std /= len(ds)\n",
    "    return mean, std\n",
    "\n",
    "# 经过 normalize 后 均值为0，方差为1\n",
    "# print(cal_mean_std(train_ds))"
   ],
   "outputs": [],
   "execution_count": 5
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:02:57.198103Z",
     "start_time": "2025-02-27T09:02:57.193935Z"
    }
   },
   "source": [
    "import torch.nn as nn\n",
    "from torch.utils.data.dataloader import DataLoader    \n",
    "\n",
    "batch_size = 16\n",
    "# 从数据集到dataloader\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)"
   ],
   "outputs": [],
   "execution_count": 6
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:02:57.357917Z",
     "start_time": "2025-02-27T09:02:57.199072Z"
    }
   },
   "source": [
    "for imgs, labels in train_loader:\n",
    "    print(imgs.shape)\n",
    "    print(labels.shape)\n",
    "    break"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([16, 3, 224, 224])\n",
      "torch.Size([16])\n"
     ]
    }
   ],
   "execution_count": 7
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:19:35.504806Z",
     "start_time": "2025-02-27T09:13:03.507435Z"
    }
   },
   "source": [
    "from torchvision.models import resnet50\n",
    "\n",
    "\n",
    "# 加载resnet50模型以及预训练权重，冻结除最后一层外所有层的权重，去除最后一层并添加自定义分类层\n",
    "class ResNet50(nn.Module):\n",
    "    def __init__(self, num_classes=10, frozen=True):\n",
    "        super().__init__()\n",
    "        # 下载预训练权重\n",
    "        self.model = resnet50(weights=\"IMAGENET1K_V2\",) # 这里的weights参数可以选择\"IMAGENET1K_V2\"或None，None表示随机初始化\n",
    "        # 冻结前面的层\n",
    "        if frozen:\n",
    "            for param in self.model.parameters():\n",
    "                param.requires_grad = False # 冻结权重\n",
    "        # for param in self.model.layer4.parameters():\n",
    "        #     param.requires_grad = True  # 解冻 layer4\n",
    "        for name, param in self.model.named_parameters():\n",
    "            if name == \"layer4.2.conv3.weight\":\n",
    "                param.requires_grad = True  # 解冻该层\n",
    "        # 添加自定义分类层\n",
    "        # print(self.model)\n",
    "        print(self.model.fc.in_features) # 打印resnet50的最后一层的输入通道数\n",
    "        print(self.model.fc.out_features) # 打印resnet50的最后一层的输出通道数 1000\n",
    "        self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)   # 自定义分类层,把resnet50的最后一层改为num_classes个输出\n",
    "        \n",
    "        \n",
    "    def forward(self, x):\n",
    "        return self.model(x)\n",
    "\n",
    "\n",
    "for idx, (key, value) in enumerate(ResNet50().named_parameters()):\n",
    "    print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")\n"
   ],
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Downloading: \"https://download.pytorch.org/models/resnet50-11ad3fa6.pth\" to C:\\Users\\13351/.cache\\torch\\hub\\checkpoints\\resnet50-11ad3fa6.pth\n",
      "100%|██████████| 97.8M/97.8M [06:30<00:00, 262kB/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n",
      "           model.conv1.weight           paramerters num: 9408\n",
      "            model.bn1.weight            paramerters num: 64\n",
      "             model.bn1.bias             paramerters num: 64\n",
      "      model.layer1.0.conv1.weight       paramerters num: 4096\n",
      "       model.layer1.0.bn1.weight        paramerters num: 64\n",
      "        model.layer1.0.bn1.bias         paramerters num: 64\n",
      "      model.layer1.0.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.0.bn2.weight        paramerters num: 64\n",
      "        model.layer1.0.bn2.bias         paramerters num: 64\n",
      "      model.layer1.0.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.0.bn3.weight        paramerters num: 256\n",
      "        model.layer1.0.bn3.bias         paramerters num: 256\n",
      "   model.layer1.0.downsample.0.weight   paramerters num: 16384\n",
      "   model.layer1.0.downsample.1.weight   paramerters num: 256\n",
      "    model.layer1.0.downsample.1.bias    paramerters num: 256\n",
      "      model.layer1.1.conv1.weight       paramerters num: 16384\n",
      "       model.layer1.1.bn1.weight        paramerters num: 64\n",
      "        model.layer1.1.bn1.bias         paramerters num: 64\n",
      "      model.layer1.1.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.1.bn2.weight        paramerters num: 64\n",
      "        model.layer1.1.bn2.bias         paramerters num: 64\n",
      "      model.layer1.1.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.1.bn3.weight        paramerters num: 256\n",
      "        model.layer1.1.bn3.bias         paramerters num: 256\n",
      "      model.layer1.2.conv1.weight       paramerters num: 16384\n",
      "       model.layer1.2.bn1.weight        paramerters num: 64\n",
      "        model.layer1.2.bn1.bias         paramerters num: 64\n",
      "      model.layer1.2.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.2.bn2.weight        paramerters num: 64\n",
      "        model.layer1.2.bn2.bias         paramerters num: 64\n",
      "      model.layer1.2.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.2.bn3.weight        paramerters num: 256\n",
      "        model.layer1.2.bn3.bias         paramerters num: 256\n",
      "      model.layer2.0.conv1.weight       paramerters num: 32768\n",
      "       model.layer2.0.bn1.weight        paramerters num: 128\n",
      "        model.layer2.0.bn1.bias         paramerters num: 128\n",
      "      model.layer2.0.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.0.bn2.weight        paramerters num: 128\n",
      "        model.layer2.0.bn2.bias         paramerters num: 128\n",
      "      model.layer2.0.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.0.bn3.weight        paramerters num: 512\n",
      "        model.layer2.0.bn3.bias         paramerters num: 512\n",
      "   model.layer2.0.downsample.0.weight   paramerters num: 131072\n",
      "   model.layer2.0.downsample.1.weight   paramerters num: 512\n",
      "    model.layer2.0.downsample.1.bias    paramerters num: 512\n",
      "      model.layer2.1.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.1.bn1.weight        paramerters num: 128\n",
      "        model.layer2.1.bn1.bias         paramerters num: 128\n",
      "      model.layer2.1.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.1.bn2.weight        paramerters num: 128\n",
      "        model.layer2.1.bn2.bias         paramerters num: 128\n",
      "      model.layer2.1.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.1.bn3.weight        paramerters num: 512\n",
      "        model.layer2.1.bn3.bias         paramerters num: 512\n",
      "      model.layer2.2.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.2.bn1.weight        paramerters num: 128\n",
      "        model.layer2.2.bn1.bias         paramerters num: 128\n",
      "      model.layer2.2.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.2.bn2.weight        paramerters num: 128\n",
      "        model.layer2.2.bn2.bias         paramerters num: 128\n",
      "      model.layer2.2.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.2.bn3.weight        paramerters num: 512\n",
      "        model.layer2.2.bn3.bias         paramerters num: 512\n",
      "      model.layer2.3.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.3.bn1.weight        paramerters num: 128\n",
      "        model.layer2.3.bn1.bias         paramerters num: 128\n",
      "      model.layer2.3.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.3.bn2.weight        paramerters num: 128\n",
      "        model.layer2.3.bn2.bias         paramerters num: 128\n",
      "      model.layer2.3.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.3.bn3.weight        paramerters num: 512\n",
      "        model.layer2.3.bn3.bias         paramerters num: 512\n",
      "      model.layer3.0.conv1.weight       paramerters num: 131072\n",
      "       model.layer3.0.bn1.weight        paramerters num: 256\n",
      "        model.layer3.0.bn1.bias         paramerters num: 256\n",
      "      model.layer3.0.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.0.bn2.weight        paramerters num: 256\n",
      "        model.layer3.0.bn2.bias         paramerters num: 256\n",
      "      model.layer3.0.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.0.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.0.bn3.bias         paramerters num: 1024\n",
      "   model.layer3.0.downsample.0.weight   paramerters num: 524288\n",
      "   model.layer3.0.downsample.1.weight   paramerters num: 1024\n",
      "    model.layer3.0.downsample.1.bias    paramerters num: 1024\n",
      "      model.layer3.1.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.1.bn1.weight        paramerters num: 256\n",
      "        model.layer3.1.bn1.bias         paramerters num: 256\n",
      "      model.layer3.1.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.1.bn2.weight        paramerters num: 256\n",
      "        model.layer3.1.bn2.bias         paramerters num: 256\n",
      "      model.layer3.1.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.1.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.1.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.2.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.2.bn1.weight        paramerters num: 256\n",
      "        model.layer3.2.bn1.bias         paramerters num: 256\n",
      "      model.layer3.2.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.2.bn2.weight        paramerters num: 256\n",
      "        model.layer3.2.bn2.bias         paramerters num: 256\n",
      "      model.layer3.2.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.2.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.2.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.3.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.3.bn1.weight        paramerters num: 256\n",
      "        model.layer3.3.bn1.bias         paramerters num: 256\n",
      "      model.layer3.3.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.3.bn2.weight        paramerters num: 256\n",
      "        model.layer3.3.bn2.bias         paramerters num: 256\n",
      "      model.layer3.3.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.3.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.3.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.4.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.4.bn1.weight        paramerters num: 256\n",
      "        model.layer3.4.bn1.bias         paramerters num: 256\n",
      "      model.layer3.4.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.4.bn2.weight        paramerters num: 256\n",
      "        model.layer3.4.bn2.bias         paramerters num: 256\n",
      "      model.layer3.4.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.4.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.4.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.5.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.5.bn1.weight        paramerters num: 256\n",
      "        model.layer3.5.bn1.bias         paramerters num: 256\n",
      "      model.layer3.5.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.5.bn2.weight        paramerters num: 256\n",
      "        model.layer3.5.bn2.bias         paramerters num: 256\n",
      "      model.layer3.5.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.5.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.5.bn3.bias         paramerters num: 1024\n",
      "      model.layer4.0.conv1.weight       paramerters num: 524288\n",
      "       model.layer4.0.bn1.weight        paramerters num: 512\n",
      "        model.layer4.0.bn1.bias         paramerters num: 512\n",
      "      model.layer4.0.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.0.bn2.weight        paramerters num: 512\n",
      "        model.layer4.0.bn2.bias         paramerters num: 512\n",
      "      model.layer4.0.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.0.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.0.bn3.bias         paramerters num: 2048\n",
      "   model.layer4.0.downsample.0.weight   paramerters num: 2097152\n",
      "   model.layer4.0.downsample.1.weight   paramerters num: 2048\n",
      "    model.layer4.0.downsample.1.bias    paramerters num: 2048\n",
      "      model.layer4.1.conv1.weight       paramerters num: 1048576\n",
      "       model.layer4.1.bn1.weight        paramerters num: 512\n",
      "        model.layer4.1.bn1.bias         paramerters num: 512\n",
      "      model.layer4.1.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.1.bn2.weight        paramerters num: 512\n",
      "        model.layer4.1.bn2.bias         paramerters num: 512\n",
      "      model.layer4.1.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.1.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.1.bn3.bias         paramerters num: 2048\n",
      "      model.layer4.2.conv1.weight       paramerters num: 1048576\n",
      "       model.layer4.2.bn1.weight        paramerters num: 512\n",
      "        model.layer4.2.bn1.bias         paramerters num: 512\n",
      "      model.layer4.2.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.2.bn2.weight        paramerters num: 512\n",
      "        model.layer4.2.bn2.bias         paramerters num: 512\n",
      "      model.layer4.2.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.2.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.2.bn3.bias         paramerters num: 2048\n",
      "            model.fc.weight             paramerters num: 20480\n",
      "             model.fc.bias              paramerters num: 10\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "execution_count": 9
  },
  {
   "cell_type": "code",
   "source": [
    "model = ResNet50(num_classes=10, frozen=True)\n",
    "def count_parameters(model): #计算模型总参数量\n",
    "    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "count_parameters(model)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-27T09:19:39.615794Z",
     "start_time": "2025-02-27T09:19:39.379520Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "1069066"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 10
  },
  {
   "cell_type": "code",
   "source": "model",
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-27T09:19:48.501369Z",
     "start_time": "2025-02-27T09:19:48.495487Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ResNet50(\n",
       "  (model): ResNet(\n",
       "    (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n",
       "    (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    (relu): ReLU(inplace=True)\n",
       "    (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n",
       "    (layer1): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "          (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer2): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer3): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (4): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (5): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer4): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))\n",
       "    (fc): Linear(in_features=2048, out_features=10, bias=True)\n",
       "  )\n",
       ")"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 11
  },
  {
   "cell_type": "code",
   "source": [
    "total_params = sum(p.numel() for p in model.parameters() )\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-27T09:19:49.747347Z",
     "start_time": "2025-02-27T09:19:49.742855Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 23528522\n"
     ]
    }
   ],
   "execution_count": 12
  },
  {
   "cell_type": "code",
   "source": [
    "m = nn.AdaptiveAvgPool2d(output_size=(1, 1))\n",
    "input = torch.randn(1, 2048, 9, 9)\n",
    "output = m(input)\n",
    "output.shape"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-27T09:19:59.321556Z",
     "start_time": "2025-02-27T09:19:59.283565Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 2048, 1, 1])"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 13
  },
  {
   "cell_type": "code",
   "source": [
    "512*3*3*512"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-27T09:19:59.975440Z",
     "start_time": "2025-02-27T09:19:59.971588Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2359296"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 14
  },
  {
   "cell_type": "code",
   "source": [
    "512*1*1*2048"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-27T09:20:00.404602Z",
     "start_time": "2025-02-27T09:20:00.399716Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1048576"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 15
  },
  {
   "cell_type": "code",
   "source": [
    "512/16"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-27T09:20:01.200373Z",
     "start_time": "2025-02-27T09:20:01.195828Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "32.0"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 16
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "pytorch的训练需要自行实现，包括\n",
    "1. 定义损失函数\n",
    "2. 定义优化器\n",
    "3. 定义训练步\n",
    "4. 训练"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:20:03.166494Z",
     "start_time": "2025-02-27T09:20:03.117671Z"
    }
   },
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "        preds = logits.argmax(axis=-1)    # 验证集预测\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "        \n",
    "    acc = accuracy_score(label_list, pred_list)\n",
    "    return np.mean(loss_list), acc\n"
   ],
   "outputs": [],
   "execution_count": 17
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TensorBoard 可视化\n",
    "\n",
    "\n",
    "训练过程中可以使用如下命令启动tensorboard服务。\n",
    "\n",
    "```shell\n",
    "tensorboard \\\n",
    "    --logdir=runs \\     # log 存放路径\n",
    "    --host 0.0.0.0 \\    # ip\n",
    "    --port 8848         # 端口\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:20:07.928555Z",
     "start_time": "2025-02-27T09:20:04.853361Z"
    }
   },
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "\n",
    "class TensorBoardCallback:\n",
    "    def __init__(self, log_dir, flush_secs=10):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            log_dir (str): dir to write log.\n",
    "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
    "        \"\"\"\n",
    "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
    "\n",
    "    def draw_model(self, model, input_shape):\n",
    "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
    "        \n",
    "    def add_loss_scalars(self, step, loss, val_loss):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/loss\", \n",
    "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
    "            global_step=step,\n",
    "            )\n",
    "        \n",
    "    def add_acc_scalars(self, step, acc, val_acc):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/accuracy\",\n",
    "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
    "            global_step=step,\n",
    "        )\n",
    "        \n",
    "    def add_lr_scalars(self, step, learning_rate):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/learning_rate\",\n",
    "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
    "            global_step=step,\n",
    "            \n",
    "        )\n",
    "    \n",
    "    def __call__(self, step, **kwargs):\n",
    "        # add loss\n",
    "        loss = kwargs.pop(\"loss\", None)\n",
    "        val_loss = kwargs.pop(\"val_loss\", None)\n",
    "        if loss is not None and val_loss is not None:\n",
    "            self.add_loss_scalars(step, loss, val_loss)\n",
    "        # add acc\n",
    "        acc = kwargs.pop(\"acc\", None)\n",
    "        val_acc = kwargs.pop(\"val_acc\", None)\n",
    "        if acc is not None and val_acc is not None:\n",
    "            self.add_acc_scalars(step, acc, val_acc)\n",
    "        # add lr\n",
    "        learning_rate = kwargs.pop(\"lr\", None)\n",
    "        if learning_rate is not None:\n",
    "            self.add_lr_scalars(step, learning_rate)\n"
   ],
   "outputs": [],
   "execution_count": 18
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Save Best\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:20:08.894276Z",
     "start_time": "2025-02-27T09:20:08.889391Z"
    }
   },
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch. \n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = -1\n",
    "        \n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "        \n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "        \n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "\n"
   ],
   "outputs": [],
   "execution_count": 19
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Early Stop"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:20:09.568250Z",
     "start_time": "2025-02-27T09:20:09.564342Z"
    }
   },
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ],
   "outputs": [],
   "execution_count": 20
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:24:37.447524Z",
     "start_time": "2025-02-27T09:20:09.966545Z"
    }
   },
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "                preds = logits.argmax(axis=-1)\n",
    "            \n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())    \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"acc\": acc, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss, val_acc = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"acc\": val_acc, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "                    \n",
    "                    # 1. 使用 tensorboard 可视化\n",
    "                    if tensorboard_callback is not None:\n",
    "                        tensorboard_callback(\n",
    "                            global_step, \n",
    "                            loss=loss, val_loss=val_loss,\n",
    "                            acc=acc, val_acc=val_acc,\n",
    "                            lr=optimizer.param_groups[0][\"lr\"],\n",
    "                            )\n",
    "                \n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)\n",
    "\n",
    "                    # 3. 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(val_acc)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 20\n",
    "\n",
    "model = ResNet50(num_classes=10)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "# 2. 定义优化器 采用 sgd\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.0)\n",
    "\n",
    "# 1. tensorboard 可视化\n",
    "if not os.path.exists(\"runs\"):\n",
    "    os.mkdir(\"runs\")\n",
    "# tensorboard_callback = TensorBoardCallback(\"runs/monkeys-resnet50\")\n",
    "# tensorboard_callback.draw_model(model, [1, 3, img_h, img_w])\n",
    "# 2. save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(\"checkpoints/monkeys-resnet50\", save_step=len(train_loader), save_best_only=True)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/1380 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "89bd8b7cb7c640f08b58f396c6dda5c2"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 11 / global_step 759\n"
     ]
    }
   ],
   "execution_count": 21
  },
  {
   "cell_type": "code",
   "source": [
    "# 画图\n",
    "\n",
    "import torch\n",
    "from torchviz import make_dot\n",
    "\n",
    "# 假设你的模型已经定义并命名为 'model'\n",
    "# 检查是否有可用的 GPU 设备\n",
    "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "\n",
    "# 将模型移动到当前设备（GPU 或 CPU）\n",
    "model.to(device)\n",
    "\n",
    "# 构造一个与模型输入形状匹配的虚拟输入\n",
    "dummy_input = torch.randn(1, 3, 224, 224).to(device)  # 将输入数据移动到与模型相同的设备\n",
    "\n",
    "# 前向传播以生成计算图\n",
    "output = model(dummy_input)\n",
    "\n",
    "# 可视化模型架构\n",
    "dot = make_dot(output, params=dict(model.named_parameters()))\n",
    "dot.render(\"model_architecture\", format=\"png\")  # 将可视化保存为图像\n"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-27T09:36:08.756308Z",
     "start_time": "2025-02-27T09:36:07.830361Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'model_architecture.png'"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 23
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:36:20.365701Z",
     "start_time": "2025-02-27T09:36:20.240194Z"
    }
   },
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    fig_num = len(train_df.columns)\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):    \n",
    "        axs[idx].plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        axs[idx].grid()\n",
    "        axs[idx].legend()\n",
    "        # axs[idx].set_xticks(range(0, train_df.index[-1], 5000))\n",
    "        # axs[idx].set_xticklabels(map(lambda x: f\"{int(x/1000)}k\", range(0, train_df.index[-1], 5000)))\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "    \n",
    "    plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=10)  #横坐标是 steps"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ],
      "image/png": "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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 24
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 评估"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-27T09:36:29.600542Z",
     "start_time": "2025-02-27T09:36:24.460858Z"
    }
   },
   "source": [
    "# dataload for evaluating\n",
    "\n",
    "# load checkpoints\n",
    "model.load_state_dict(torch.load(\"checkpoints/monkeys-resnet50/best.ckpt\", map_location=\"cpu\"))\n",
    "\n",
    "model.eval()\n",
    "loss, acc = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\\naccuracy: {acc:.4f}\")"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.3131\n",
      "accuracy: 0.9926\n"
     ]
    }
   ],
   "execution_count": 25
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
